package munci.core;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;

public class Dictionary {
	
	@Documented 
	@Retention(RetentionPolicy.RUNTIME)
	@Target(ElementType.FIELD)
	public @interface Name {
		String value();
	}
	
	private final Dictionary next;
	
	public Dictionary() {
		this(null);
	}
	
	public Dictionary(Dictionary next) {
		this.next = next;
	}
	
	public static class Entry {
		public final String name;
		public final Field reference;
		public final Word word;
		private Entry next;
		
		private Entry(String name, Field reference, Word word, Entry next) {
			this.name = name;
			this.reference = reference;
			this.word = word;
			this.next = next;
		}
		
		static Entry add(Entry entry, String name, Field reference, Word word) {
			if (entry == null || word.hasAttribute(entry.word)) {
				return new Entry(name, reference, word, entry);
			} else {
				entry.next = add(entry.next, name, reference, word);
				return entry;
			}
		}
		
		@Override public String toString() {
			return name;
		}
	}
	
	private final Map<String, Entry> entries = new HashMap<String, Entry>();

	public Dictionary scan(Class<?> type) {
		for (Field field : type.getFields()) {
			Name name = field.getAnnotation(Name.class);
			int mod = field.getModifiers();
			if (name != null) {
				if (Modifier.isStatic(mod) && Modifier.isFinal(mod) && Modifier.isPublic(mod) &&
						Word.class.isAssignableFrom(field.getType())) {
					try {
						add(name.value(), field, (Word) field.get(null));
					} catch (Exception e) {
						throw new RuntimeException(e);
					}
				} else {
					throw new RuntimeException("Invalid field declaration: " + field.getName());
				}
			}
		}
		return this;
	}
	
	public Dictionary add(String name, Field reference, Word word) {
		entries.put(name, Entry.add(entries.get(name), name, reference, word));
		return Dictionary.this;
	}
	
	public Entry find(String name, Word.R1<Boolean, Word> test) {
		for (Entry entry = entries.get(name); entry != null; entry = entry.next) {
			if (test.call(entry.word) == Boolean.TRUE) return entry;
		}
		return next == null? null : next.find(name, test);
	}
	
	public Dictionary next() {
		return next;
	}
	
	@Override public String toString() {
		StringBuilder str = new StringBuilder();
		String sep = "";
		for (String name : entries.keySet()) {
			str.append(sep).append(name);
			sep = " ";
		}
		if (next != null) str.append('\n').append(next.toString());
		return str.toString();
	}
}
